--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit ec44e225a10a509e71b771c26c08245d13c3bc48
Parents : a8a9eb3
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-05-06T00:22:08-05:00
feat(NotificationUtils): add comprehensive tests for notification handling across Electron, Android, and browser fallback
Changes
2 files changed, 135 insertions(+), 0 deletions(-)
Diff
diff --git a/tests/frontend/NotificationUtils.test.js b/tests/frontend/NotificationUtils.test.js
new file mode 100644
index 00000000..1da598fb
--- /dev/null
+++ b/tests/frontend/NotificationUtils.test.js
@@ -0,0 +1,127 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import NotificationUtils from "../../meshchatx/src/frontend/js/NotificationUtils";
+
+describe("NotificationUtils", () => {
+ let originalNotification;
+ let electronMock;
+ let androidMock;
+
+ beforeEach(() => {
+ originalNotification = globalThis.Notification;
+ electronMock = { showNotification: vi.fn() };
+ androidMock = {
+ getPlatform: vi.fn().mockReturnValue("android"),
+ showNotification: vi.fn(),
+ showIncomingCallNotification: vi.fn(),
+ showMissedCallNotification: vi.fn(),
+ cancelIncomingCallNotification: vi.fn(),
+ };
+ globalThis.Notification = vi.fn(function (title, opts) {
+ return { title, opts };
+ });
+ globalThis.Notification.requestPermission = vi.fn().mockResolvedValue("granted");
+ });
+
+ afterEach(() => {
+ globalThis.Notification = originalNotification;
+ delete globalThis.electron;
+ delete globalThis.MeshChatXAndroid;
+ vi.restoreAllMocks();
+ });
+
+ describe("Electron", () => {
+ beforeEach(() => {
+ globalThis.electron = electronMock;
+ });
+
+ it("showNewMessageNotification delegates to electron", () => {
+ NotificationUtils.showNewMessageNotification("Alice", "hello");
+ expect(electronMock.showNotification).toHaveBeenCalledWith("New Message", "Alice: hello");
+ });
+
+ it("showIncomingCallNotification delegates to electron", () => {
+ NotificationUtils.showIncomingCallNotification("Bob");
+ expect(electronMock.showNotification).toHaveBeenCalledWith("Incoming Call", "Bob is calling you.");
+ });
+
+ it("showMissedCallNotification delegates to electron", () => {
+ NotificationUtils.showMissedCallNotification("Charlie");
+ expect(electronMock.showNotification).toHaveBeenCalledWith(
+ "Missed Call",
+ "You missed a call from Charlie."
+ );
+ });
+
+ it("showNewVoicemailNotification delegates to electron", () => {
+ NotificationUtils.showNewVoicemailNotification("Dave");
+ expect(electronMock.showNotification).toHaveBeenCalledWith(
+ "New Voicemail",
+ "You have a new voicemail from Dave."
+ );
+ });
+ });
+
+ describe("Android", () => {
+ beforeEach(() => {
+ globalThis.MeshChatXAndroid = androidMock;
+ });
+
+ it("showNewMessageNotification delegates to Android bridge", () => {
+ NotificationUtils.showNewMessageNotification("Alice", "hello");
+ expect(androidMock.showNotification).toHaveBeenCalledWith("New Message", "Alice: hello");
+ });
+
+ it("showIncomingCallNotification delegates to Android bridge", () => {
+ NotificationUtils.showIncomingCallNotification("Bob");
+ expect(androidMock.showIncomingCallNotification).toHaveBeenCalledWith("Bob");
+ });
+
+ it("showMissedCallNotification delegates to Android bridge", () => {
+ NotificationUtils.showMissedCallNotification("Charlie");
+ expect(androidMock.showMissedCallNotification).toHaveBeenCalledWith(
+ "Missed Call",
+ "You missed a call from Charlie."
+ );
+ });
+
+ it("showNewVoicemailNotification delegates to Android bridge", () => {
+ NotificationUtils.showNewVoicemailNotification("Dave");
+ expect(androidMock.showNotification).toHaveBeenCalledWith(
+ "New Voicemail",
+ "You have a new voicemail from Dave."
+ );
+ });
+
+ it("cancelIncomingCallNotification delegates to Android bridge", () => {
+ NotificationUtils.cancelIncomingCallNotification();
+ expect(androidMock.cancelIncomingCallNotification).toHaveBeenCalled();
+ });
+ });
+
+ describe("Browser fallback", () => {
+ it("showNewMessageNotification uses browser Notification API", async () => {
+ NotificationUtils.showNewMessageNotification("Alice", "hello");
+ await new Promise((r) => setTimeout(r, 10));
+ expect(globalThis.Notification).toHaveBeenCalledWith(
+ "New Message",
+ expect.objectContaining({ body: "Alice: hello" })
+ );
+ });
+ });
+
+ describe("_isAndroid detection", () => {
+ it("returns false when MeshChatXAndroid is missing", () => {
+ expect(NotificationUtils._isAndroid()).toBe(false);
+ });
+
+ it("returns false when getPlatform returns non-android", () => {
+ globalThis.MeshChatXAndroid = { getPlatform: () => "ios" };
+ expect(NotificationUtils._isAndroid()).toBe(false);
+ });
+
+ it("returns true when getPlatform returns android", () => {
+ globalThis.MeshChatXAndroid = androidMock;
+ expect(NotificationUtils._isAndroid()).toBe(true);
+ });
+ });
+});
diff --git a/tests/frontend/Toast.test.js b/tests/frontend/Toast.test.js
index 95c2bacb..ddc3a4d1 100644
--- a/tests/frontend/Toast.test.js
+++ b/tests/frontend/Toast.test.js
@@ -99,4 +99,12 @@ describe("Toast.vue", () => {
const toast = wrapper.find(".pointer-events-auto");
expect(toast.find("button").exists()).toBe(true);
});
+
+ it("positions container with mobile-safe bottom offset", () => {
+ const container = wrapper.find("[class*='fixed']");
+ expect(container.exists()).toBe(true);
+ const cls = container.classes().join(" ");
+ expect(cls).toContain("max-sm:bottom-");
+ expect(cls).not.toContain("max-sm:bottom-[calc(5.75rem");
+ });
});
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────